﻿/*******************************************************************************
 * AAEngine
 * Copyright (c) 2010 Mike Jarosch
 *
 * Original source PushButton Engine:
 * Copyright (C) 2009 PushButton Labs, LLC
 * For more information see http://www.pushbuttonengine.com
 *
 * This file is licensed under the terms of the MIT license, which is included
 * in the "MIT License.txt" file at the root directory of this SDK.
 ******************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Xml.Linq;
using AAEngine.Engine.Core;
using AAEngine.Engine.Debug;
using AAEngine.Engine.Entities;

namespace AAEngine.Engine.Serialization
{
    public static class Serializer
    {
        private static IEntity _currentEntity = null;
        private static Dictionary<string, Action<object, XElement>> _serializers = new Dictionary<string, Action<object, XElement>>();
        private static Dictionary<string, Func<object, XElement, string, object>> _deserializers = new Dictionary<string, Func<object, XElement, string, object>>();
        private static List<ReferenceNote> _deferredReferences = new List<ReferenceNote>();

        static Serializer()
        {
            _deserializers["::DefaultSimple"] = DeserializeSimple;
            _deserializers["::DefaultComplex"] = DeserializeComplex;
            _deserializers["System.Boolean"] = DeserializeBoolean;
            _deserializers["System.Array"] = DeserializeArray;
            _deserializers["System.Collections.Hashtable"] = DeserializeHashtable;

            _serializers["::DefaultSimple"] = SerializeSimple;
            _serializers["::DefaultComplex"] = SerializeComplex;
            _serializers["System.Boolean"] = SerializeBoolean;
            _serializers["System.Array"] = SerializeArray;
            _serializers["System.Collections.Hashtable"] = SerializeHashtable;
        }

        public static void Serialize(object obj, XElement xml)
        {
            if (obj is ISerializable)
            {
                ((ISerializable)obj).Serialize(xml);
            }
            else if (obj is IEntity)
            {
                _currentEntity = (IEntity)obj;
                _currentEntity.Serialize(xml);
            }
            else
            {
                string typeName = obj.GetType().FullName;
                if (!_serializers.ContainsKey(typeName))
                {
                    typeName = IsSimpleType(obj) ? "::DefaultSimple" : "::DefaultComplex";
                }
                _serializers[typeName](obj, xml);
            }
        }

#if WINDOWS_PHONE || XBOX
        public static object Deserialize(object obj, XElement xml)
        {
            return Deserialize(obj, xml, null);
        }

        public static object Deserialize(object obj, XElement xml, string typeHint)
#else
        public static object Deserialize(object obj, XElement xml, string typeHint = null)
#endif
        {
            if (obj is ISerializable)
            {
                return ((ISerializable)obj).Deserialize(xml);
            }
            else if (obj is IEntity)
            {
                _currentEntity = (IEntity)obj;
                _currentEntity.Deserialize(xml, true);
                ResolveReferences();
                return obj;
            }
            else
            {
                string typeName = obj.GetType().FullName;
                if (!_serializers.ContainsKey(typeName))
                {
                    if (obj.GetType().IsArray)
                    {
                        typeName = "System.Array";
                    }
                    else
                    {
                        typeName = IsSimpleType(obj) ? "::DefaultSimple" : "::DefaultComplex";
                    }
                }
                return _deserializers[typeName](obj, xml, typeHint);
            }
        }

        public static void SetCurrentEntity(IEntity e)
        {
            _currentEntity = e;
        }

        public static void ClearCurrentEntity()
        {
            _currentEntity = null;
        }

        public static void ReportMissingReferences()
        {
            foreach (ReferenceNote note in _deferredReferences)
            {
                note.ReportMissing();
            }
        }

        private static bool IsSimple(XElement xml, string typeName)
        {
            if (typeName == "String")
            {
                return true;
            }

            if (xml.HasElements)
            {
                return false;
            }

            if (xml.ToString() == "")
            {
                return false;
            }

            return true;
        }

        private static bool IsSimpleType(object obj)
        {
            if (obj is string || obj is int || obj is float || obj is bool || obj is Enum)
            {
                return true;
            }
            return false;
        }

        private static object DeserializeSimple(object obj, XElement xml, string typeHint)
        {
            if (xml.Value == "" && !(obj is String))
            {
                return obj;
            }

            if (!String.IsNullOrEmpty(typeHint) && typeHint != "System.String")
            {
                if (TypeUtility.GetTypeFromName(typeHint).IsEnum)
                {
                    return Enum.Parse(TypeUtility.GetTypeFromName(typeHint), xml.Value, true);
                }
                else
                {
                    return Convert.ChangeType(xml.Value, TypeUtility.GetTypeFromName(typeHint), null);
                }
            }
            else
            {
                return xml.Value;
            }
        }

        private static void SerializeSimple(object obj, XElement xml)
        {
            xml.Value = obj.ToString();
        }

        private static object DeserializeComplex(object obj, XElement xml, string typeHint)
        {
            string xmlPath = "";

            foreach(XElement node in xml.Elements())
            {
                string fieldName = node.Name.LocalName;

                if (!TypeUtility.HasProperty(obj, fieldName))
                {
                    // Try capitalizing the first letter
                    string cappedFieldName = fieldName[0].ToString().ToUpperInvariant() + fieldName.Substring(1);

                    if (TypeUtility.HasProperty(obj, cappedFieldName))
                    {
                        fieldName = cappedFieldName;
                    }
                    else
                    {
                        xmlPath = ReportXmlPath(node);
                        Logger.Warn(obj, "DeserializeComplex", "The field '" + fieldName + "' does not exist on the class '" + TypeUtility.GetClassName(obj) + "'. " + xmlPath);
                        continue;
                    }
                }

                string typeName = node.GetAttribute("type");
                if (String.IsNullOrEmpty(typeName))
                {
                    typeName = TypeUtility.GetFieldType(obj, fieldName);
                }

                if (!GetChildReference(obj, fieldName, node) && !GetResourceObject(obj, fieldName, node))
                {
                    object child = GetChildObject(obj, fieldName, typeName, node);
                    if (child != null)
                    {
                        string childTypeHint = TypeUtility.GetFieldType(obj, fieldName);
                        child = Deserialize(child, node, childTypeHint);
                    }

                    try
                    {
                        TypeUtility.SetProperty(obj, fieldName, child);
                    }
                    catch (Exception ex)
                    {
                        Logger.Warn(obj, "DeserializeComplex", "The field '" + fieldName + "' could not be set to '" + child.ToString() + "' due to :" + ex.Message + ".");
                    }
                }
            }

            return obj;
        }

        private static void SerializeComplex(object obj, XElement xml)
        {
            // TODO
        }

        private static object DeserializeBoolean(object obj, XElement xml, string typeHint)
        {
            return xml.Value == "true";
        }

        private static void SerializeBoolean(object obj, XElement xml)
        {
            if ((bool)obj)
            {
                xml.Value = "true";
            }
            else
            {
                xml.Value = "false";
            }
        }

        private static object DeserializeArray(object obj, XElement xml, string typeHint)
        {
            foreach (XElement child in xml.Elements())
            {
                string key = child.Name.LocalName;

                if(key[0] == '_')
                {
                    key = key.Substring(1);
                }

                string typeName = child.GetAttribute("type");
                if (String.IsNullOrEmpty(typeName))
                {
                    typeName = xml.GetAttribute("childType");
                }
                if (String.IsNullOrEmpty(typeName))
                {
                    typeName = String.IsNullOrEmpty(typeHint) ? "String" : typeHint;
                }

                // do we need to grow the array
                if (key.Length > 0)
                {
                    int index = Int32.Parse(key);
                    if (index >= (obj as Array).Length)
                    {
                        Array temp = Array.CreateInstance(obj.GetType().GetElementType(), index + 1);
                        for (int i = 0; i < (obj as Array).Length; i++)
                        {
                            temp.SetValue((obj as Array).GetValue(i), i);
                        }
                        obj = temp;
                    }
                }
                else
                {
                    int index = (obj as Array).Length;
                    Type elementType = (obj as Array).GetType().GetElementType();
                    Array temp = Array.CreateInstance(elementType, index + 1);
                    for (int i = 0; i < (obj as Array).Length; i++)
                    {
                        temp.SetValue((obj as Array).GetValue(i), i);
                    }
                    obj = temp;

                    key = index.ToString();
                }

                if (!GetChildReference(obj, key, child) && !GetResourceObject(obj, key, child, typeHint))
                {
                    object value = GetChildObject(obj, key, typeName, child);

                    if (value != null)
                    {
                        value = Deserialize(value, child);
                    }

                    TypeUtility.SetProperty(obj, key, value);
                }
            }

            return obj;
        }

        private static void SerializeArray(object obj, XElement xml)
        {
            // TODO
            throw new NotImplementedException();
        }

        private static object DeserializeHashtable(object obj, XElement xml, string typeHint)
        {
            foreach (XElement child in xml.Elements())
            {
                string key = child.Name.LocalName;

                if (key[0] == '_')
                {
                    key = key.Substring(1);
                }

                if (key.Length < 1)
                {
                    string xmlPath = ReportXmlPath(child);
                    Logger.Error(obj, "DeserializeHashtable", "Cannot add a value to a dictionary without a key. " + xmlPath);
                    continue;
                }

                string typeName = child.GetAttribute("type");
                if (String.IsNullOrEmpty(typeName))
                {
                    typeName = xml.GetAttribute("childType");
                }
                if (String.IsNullOrEmpty(typeName))
                {
                    typeName = String.IsNullOrEmpty(typeHint) ? "String" : typeHint;
                }

                if (!GetChildReference(obj, key, child) && !GetResourceObject(obj, key, child, typeName))
                {
                    object value = GetChildObject(obj, key, typeName, child);

                    if (value != null)
                    {
                        value = Deserialize(value, child);
                    }

                    (obj as Hashtable)[key] = value;
                }
            }

            return obj;
        }

        private static void SerializeHashtable(object obj, XElement xml)
        {
            // TODO
            throw new NotImplementedException();
        }

        private static string ReportXmlPath(XElement node)
        {
            StringBuilder path =  new StringBuilder("(");

            List<XElement> stack = new List<XElement>();
            XElement itemWalk = node;
            while (itemWalk != null)
            {
                stack.Insert(0, itemWalk);
                itemWalk = itemWalk.Parent;
            }

            for (int i = 0; i < stack.Count; i++)
            {
                XElement x = stack[i];

                path.Append("<" + x.Name);

                if (x.HasAttribute("name"))
                {
                    path.Append(" name=\"" + x.GetAttribute("name") + "\"");
                }

                path.Append(">");

                if (i < stack.Count - 1)
                {
                    path.Append(" --> ");
                }
            }

            path.Append(")");

            return path.ToString();
        }

        private static void ResolveReferences()
        {
            for (int i = 0; i < _deferredReferences.Count; i++)
            {
                ReferenceNote reference = _deferredReferences[i];
                if (reference.Resolve())
                {
                    _deferredReferences.RemoveAt(i);
                    i--;
                }
            }
        }

        private static bool GetChildReference(object obj, string fieldName, XElement xml)
        {
            string nameReference = xml.GetAttribute("nameReference");
            string componentReference = xml.GetAttribute("componentReference");
            if (string.IsNullOrEmpty(componentReference))
            {
                componentReference = xml.GetAttribute("entityName");
            }

            string componentName = xml.GetAttribute("componentName");
            string objectReference = xml.GetAttribute("objectReference");

            if (!string.IsNullOrEmpty(nameReference) || !string.IsNullOrEmpty(componentReference) || !string.IsNullOrEmpty(componentName) || !string.IsNullOrEmpty(objectReference))
            {
                ReferenceNote reference = new ReferenceNote();
                reference.Owner = obj;
                reference.FieldName = fieldName;
                reference.NameReference = nameReference;
                reference.ComponentReference = componentReference;
                reference.ComponentName = componentName;
                reference.ObjectReference = objectReference;
                reference.CurrentEntity = _currentEntity;

                if (!reference.Resolve())
                {
                    _deferredReferences.Add(reference);
                }

                return true;
            }

            return false;
        }

#if WINDOWS_PHONE || XBOX
        private static bool GetResourceObject(object obj, string fieldName, XElement xml)
        {
            return GetResourceObject(obj, fieldName, xml, null);
        }

        private static bool GetResourceObject(object obj, string fieldName, XElement xml, string typeHint)
#else
        private static bool GetResourceObject(object obj, string fieldName, XElement xml, string typeHint = null)
#endif
        {
            string filename = xml.GetAttribute("filename");

            if (String.IsNullOrEmpty(filename))
            {
                XElement child = xml.Element("filename");
                if (child != null)
                {
                    filename = child.Value;
                }
            }

            if (String.IsNullOrEmpty(filename))
            {
                return false;
            }

            Type type = null;
            if (typeHint != null)
            {
                type = TypeUtility.GetTypeFromName(typeHint);
            }
            else
            {
                type = TypeUtility.GetTypeFromName(TypeUtility.GetFieldType(obj, fieldName));
            }

            object resource = AAGame.ResourceManager.Load(filename, type);
            TypeUtility.SetProperty(obj, fieldName, resource);

            return true;
        }

        /// <summary>
        /// Find or instantiate the value that should go in a named field on an object.
        /// </summary>
        /// <param name="obj">The object which will have the object assigned.</param>
        /// <param name="fieldName">e field on the object we are working with.</param>
        /// <param name="typeName">The desired type</param>
        /// <param name="xml"></param>
        /// <returns></returns>
        private static object GetChildObject(object obj, string fieldName, string typeName, XElement xml)
        {
            object childObject = TypeUtility.GetProperty(obj, fieldName);

            if (childObject == null)
            {
                childObject = TypeUtility.Instantiate(typeName);
            }

            if (childObject == null)
            {
                string xmlPath = ReportXmlPath(xml);
                Logger.Error(obj, "GetChildObject", "Unable to create type '" + typeName + "' for the field '" + fieldName + "'. " + xmlPath);
                return null;
            }

            return childObject;
        }
    }
}
